using System;
using System.Collections.Generic;
using System.Linq;
using Microsoft.Xna.Framework;
using Microsoft.Xna.Framework.Audio;
using Microsoft.Xna.Framework.Content;
using Microsoft.Xna.Framework.GamerServices;
using Microsoft.Xna.Framework.Graphics;
using Microsoft.Xna.Framework.Input;
using Microsoft.Xna.Framework.Media;
using System.IO;

using SynapseGaming.LightingSystem.Core;
using SynapseGaming.LightingSystem.Editor;
using SynapseGaming.LightingSystem.Lights;
using SynapseGaming.LightingSystem.Rendering;

using SunburnJitter;

namespace SunburnJitterDemo
{
    /// <summary>
    /// This is the main type for your game
    /// </summary>
    public class Game1 : Microsoft.Xna.Framework.Game
    {
        // The SunBurn lighting system.
        LightingSystemManager lightingSystemManager;
        ContentRepository contentRepository;
        SceneInterface sceneInterface;
        FrameBuffers frameBuffers;
        LightingSystemEditor editor;
        SceneState sceneState;
        SceneEnvironment environment;
        LightingSystemPreferences preferences;
        SplashScreenGameComponent splashScreenGameComponent;

        // Scene/camera supporting members.
        bool firstMouseSample = true;
        Vector3 viewPosition = new Vector3(3.0f, -.5f, -0.0f);
        Vector3 viewRotation = new Vector3(-3.14f, 0.316f, 0.0f);
        Matrix view = Matrix.Identity;
        Matrix projection = Matrix.Identity;

        // Default XNA members.
        GraphicsDeviceManager graphics;
        SpriteBatch spriteBatch;

        const float moveScale = 100.0f;
        const string userPreferencesFile = "UserPreferences.xml";

        JitterCollisionManager collision;

        KeyboardState prevKeyboardState;    //The keyboard state of the previous frame

        public Game1()
        {
            graphics = new GraphicsDeviceManager(this);
            Content.RootDirectory = "Content";

            graphics.PreferredBackBufferWidth = 800;
            graphics.PreferredBackBufferHeight = 600;
            graphics.PreferMultiSampling = false;
            graphics.SynchronizeWithVerticalRetrace = false;

            // Improve overall performance.
            graphics.PreparingDeviceSettings += PrepareDeviceSettings;

            // Used for advanced edge cleanup and deferred performance optimizations.
            graphics.PreferredDepthStencilFormat = DepthFormat.Depth24Stencil8;

            // Required for lighting system.
            splashScreenGameComponent = new SplashScreenGameComponent(this, graphics);
            Components.Add(splashScreenGameComponent);

            // Create the lighting system.
            lightingSystemManager = new LightingSystemManager(Services, Content);
            sceneState = new SceneState();

            // Create the scene interface. Acts as a service provider containing all scene managers
            // and returning them by type (including custom managers). Also acts as a component
            // container where calls to manager methods on the SceneInterface (such as BeginFrameRendering,
            // Unload, ...) are automatically called on all contained managers.
            //
            // This design allows managers to be plugged-in like modular components and for managers
            // to easily be added, removed, or replaced with custom implementations.
            //
            sceneInterface = new SceneInterface(graphics);
            sceneInterface.CreateDefaultManagers(false);


            // Post console messages letting the user know how to open the SunBurn Editor.
            sceneInterface.ShowConsole = true;
            SystemConsole.AddMessage("Welcome to the SunburnJitter Demo.", 4);
            SystemConsole.AddMessage("Use an Xbox controller or the W, A, S, D keys to navigate the scene.", 8);
            SystemConsole.AddMessage("Press F11 to open the SunBurn Editor.", 12);
            //SystemConsole.AddMessage("Press F11 to open the SunBurn Editor.", 20);

            // Create the frame buffers used for rendering (sized to the backbuffer) and
            // assign them to the ResourceManager so we don't have to worry about cleanup.
#if WINDOWS_PHONE
            // Setting the buffers to a lower power of two to boost bloom performance.
            frameBuffers = new FrameBuffers(graphics, 512, 256, DetailPreference.High, DetailPreference.High);
#else
            frameBuffers = new FrameBuffers(graphics, DetailPreference.High, DetailPreference.High);
#endif
            sceneInterface.ResourceManager.AssignOwnership(frameBuffers);

            // Create the editor and add it to the scene interface (this is not included
            // in the default manager set).
            editor = new LightingSystemEditor(sceneInterface, Services, graphics, this);
            sceneInterface.AddManager(editor);

            //First remove sunburn collision manager
            sceneInterface.RemoveManager(sceneInterface.CollisionManager);
            collision = new JitterCollisionManager(sceneInterface);
            sceneInterface.AddManager(collision);

            // Set the in-editor camera speed, icon size, and hotkey.
            editor.MoveScale = moveScale;
            editor.LaunchKey = Keys.F11;

            // The skybox handles the back buffer clear.
            if (sceneInterface.RenderManager is BaseRenderManager)
                (sceneInterface.RenderManager as BaseRenderManager).ClearBackBufferEnabled = false;

#if USE_DEFERRED
            // Scene specific optimizations.
            if (sceneInterface.RenderManager is DeferredRenderManager)
            {
                DeferredRenderManager deferredRenderManager = sceneInterface.RenderManager as DeferredRenderManager;
                deferredRenderManager.DepthFillOptimizationEnabled = true;
                deferredRenderManager.OcclusionQueryEnabled = false;
            }
#endif

            // Load the user preferences (example - not required).
            preferences = new LightingSystemPreferences();
#if !XBOX && !WINDOWS_PHONE
            if (File.Exists(userPreferencesFile))
                preferences.LoadFromFile(userPreferencesFile);
            else
#endif
            {
                preferences.EffectDetail = DetailPreference.Medium;
                preferences.MaxAnisotropy = 4;
                preferences.PostProcessingDetail = DetailPreference.High;
                preferences.ShadowDetail = DetailPreference.Medium;
                preferences.ShadowQuality = 1.0f;
                preferences.TextureSampling = SamplingPreference.Anisotropic;
            }

            view = GetViewMatrix();
        }

        /// <summary>
        /// Improves overall performance with dynamic shadows.
        /// </summary>
        private void PrepareDeviceSettings(object sender, PreparingDeviceSettingsEventArgs e)
        {
            e.GraphicsDeviceInformation.PresentationParameters.RenderTargetUsage = RenderTargetUsage.PlatformContents;
        }

        /// <summary>
        /// Allows the game to perform any initialization it needs to before starting to run.
        /// This is where it can query for any required services and load any non-graphic
        /// related content.  Calling base.Initialize will enumerate through any components
        /// and initialize them as well.
        /// </summary>
        protected override void Initialize()
        {
            // TODO: Add your initialization logic here
            prevKeyboardState = Keyboard.GetState();
            collision.Initialize();
            base.Initialize();
        }

        /// <summary>
        /// LoadContent will be called once per game and is the place to load
        /// all of your content.
        /// </summary>
        protected override void LoadContent()
        {
            // Load the content repository, which stores all assets imported via the editor.
            // This must be loaded before any other assets.
            contentRepository = Content.Load<ContentRepository>("ContentRepository");

            // Create a new SpriteBatch, which can be used to draw textures.
            spriteBatch = new SpriteBatch(GraphicsDevice);

            //Load and submit scene for light:
            Scene scene = Content.Load<Scene>("Scene");
            
            sceneInterface.ObjectManager.Submit(scene);
            sceneInterface.LightManager.Submit(scene);

            //Create a static object (can also be done in editor):
            SceneObject staticObj = new SceneObject(Content.Load<Model>("Models/Level/room"));
            staticObj.Visibility = ObjectVisibility.RenderedAndCastShadows;
            staticObj.World = Matrix.CreateScale(0.01f) * Matrix.CreateRotationY(0.5f) * Matrix.CreateTranslation(0, 5, 0);    //scale it down a bit
            sceneInterface.ObjectManager.Submit(staticObj);
            staticObj.Components.Add(new TriangleMeshRigidBodyComponent());
            //collision.SubmitStatic(staticObj);  //Tell the collision system to use this scene object as a static mesh

            environment = Content.Load<SceneEnvironment>("Environment");
            collision.Gravity = new Vector3(0, -environment.Gravity, 0);

            sceneInterface.ApplyPreferences(preferences);
        }

        /// <summary>
        /// UnloadContent will be called once per game and is the place to unload
        /// all content.
        /// </summary>
        protected override void UnloadContent()
        {
            // TODO: Unload any non ContentManager content here

            sceneInterface.Unload();
            lightingSystemManager.Unload();

            environment = null;
        }

        /// <summary>
        /// Allows the game to run logic such as updating the world,
        /// checking for collisions, gathering input, and playing audio.
        /// </summary>
        /// <param name="gameTime">Provides a snapshot of timing values.</param>
        protected override void Update(GameTime gameTime)
        {
            KeyboardState keystate = Keyboard.GetState();
            // Allows game controls when not editing the scene.
            if (!editor.EditorAttached)
            {
                if (GamePad.GetState(PlayerIndex.One).Buttons.Back == ButtonState.Pressed ||
                    Keyboard.GetState().IsKeyDown(Keys.Escape))
                    this.Exit();

                view = ProcessCameraInput(gameTime);
            }

            projection = Matrix.CreatePerspectiveFieldOfView(MathHelper.ToRadians(70.0f),
                graphics.GraphicsDevice.Viewport.AspectRatio, 0.1f, environment.VisibleDistance);

            sceneInterface.Update(gameTime);

            //Enables/Disables CollisionManagers debug draw mode, you can also set it manually for each
            //object: rigidBodyComponent.DebugDraw = true;
            if (keystate.IsKeyDown(Keys.Tab) && !prevKeyboardState.IsKeyDown(Keys.Tab))
                collision.DebugDrawAll = !collision.DebugDrawAll;


            if (keystate.IsKeyDown(Keys.Space) && !prevKeyboardState.IsKeyDown(Keys.Space)) {
                //Create a dynamic scene object:
                SceneObject obj = new SceneObject(Content.Load<Model>("Models/Cube"));
                obj.Visibility = ObjectVisibility.RenderedAndCastShadows;
                obj.ObjectType = ObjectType.Dynamic;
                //Scale it a bit and translate it to camera position
                obj.World = Matrix.CreateScale(5) * Matrix.CreateTranslation(viewPosition);
                sceneInterface.ObjectManager.Submit(obj);

                //The physical system is Component based, so create a new BoxRigidBodyComponent, and add it to
                //the component system. The rest will be down automaticly (updating world matrix, etc.)
                RigidBodyComponent rigidBody = new BoxRigidBodyComponent(13, 13, 13);
                //rigidBody = new ConeRigidBodyComponent(20, 8, new Vector3(0, -2, 0));
                obj.Components.Add(rigidBody);
                
                //Now apply a force to the box, so it should be throwed into the camera view's direction.
                Vector3 force = -Vector3.Transform(Vector3.Forward, Quaternion.CreateFromYawPitchRoll(viewRotation.X, viewRotation.Y, viewRotation.Z));
                rigidBody.RigidBody.LinearVelocity = (force * 100).ToJVector();
            }

            prevKeyboardState = keystate;
            base.Update(gameTime);
        }

        /// <summary>
        /// This is called when the game should draw itself.
        /// </summary>
        /// <param name="gameTime">Provides a snapshot of timing values.</param>
        protected override void Draw(GameTime gameTime)
        {
            // Check to see if the splash screen is finished.
            if (!SplashScreenGameComponent.DisplayComplete)
            {
                base.Draw(gameTime);
                return;
            }

            // Render the scene.
            sceneState.BeginFrameRendering(view, projection, gameTime, environment, frameBuffers, true);
            sceneInterface.BeginFrameRendering(sceneState);

            GraphicsDevice.Clear(ClearOptions.Target | ClearOptions.DepthBuffer | ClearOptions.Stencil, Color.CornflowerBlue, 1.0f, 0);

            sceneInterface.RenderManager.Render();

            //Do the debug drawing stuff of the collision manager:
            collision.Draw(view, projection);

            sceneInterface.EndFrameRendering();
            sceneState.EndFrameRendering();

            base.Draw(gameTime);
        }

        /// <summary>
        /// Handles controller input.
        /// </summary>
        /// <param name="gameTime">Provides a snapshot of timing values.</param>
        public Matrix ProcessCameraInput(GameTime gameTime)
        {
            if (IsActive)
            {
                GamePadState gamepad = GamePad.GetState(PlayerIndex.One);
                KeyboardState keyboard = Keyboard.GetState();
                MouseState mouse = Mouse.GetState();

                float timescale = (float)gameTime.ElapsedGameTime.TotalSeconds;
                float rotatescale = 3.0f * timescale;
                float movescale = timescale * moveScale;

                // Get the right trigger, which affects speed.
                if (gamepad.IsConnected)
                {
                    rotatescale *= (1.0f - gamepad.Triggers.Right * 0.5f);
                    movescale *= (1.0f - gamepad.Triggers.Right * 0.5f);
                }
                else if (mouse.RightButton == ButtonState.Pressed)
                    movescale *= 0.25f;

                // If the gamepad is connected use its input instead of the mouse and keyboard.
                if (gamepad.IsConnected)
                    viewRotation -= new Vector3(gamepad.ThumbSticks.Right.X * rotatescale, gamepad.ThumbSticks.Right.Y * rotatescale, 0.0f);
                else
                {
                    GraphicsDevice device = graphics.GraphicsDevice;
                    int halfx = device.Viewport.Width / 2;
                    int halfy = device.Viewport.Height / 2;

                    if (!firstMouseSample)
                    {
                        // Convert the amount the mouse was moved into camera rotation.
                        viewRotation.X += MathHelper.ToRadians((float)(halfx - mouse.X) * rotatescale * 1.5f);
                        viewRotation.Y -= MathHelper.ToRadians((float)(halfy - mouse.Y) * rotatescale * 1.5f);
                    }
                    else
                        firstMouseSample = false;

                    Mouse.SetPosition(halfx, halfy);
                }

                if (viewRotation.Y > MathHelper.PiOver2 - 0.01f)
                    viewRotation.Y = MathHelper.PiOver2 - 0.01f;
                else if (viewRotation.Y < -MathHelper.PiOver2 + 0.01f)
                    viewRotation.Y = -MathHelper.PiOver2 + 0.01f;

                Quaternion rot = Quaternion.CreateFromYawPitchRoll(viewRotation.X, viewRotation.Y, viewRotation.Z);

                // Now apply the camera movement based on either the gamepad or keyboard input.
                if (gamepad.IsConnected)
                {
                    viewPosition += Vector3.Transform(new Vector3(movescale, 0, movescale) * new Vector3(
                        -gamepad.ThumbSticks.Left.X, 0,
                        gamepad.ThumbSticks.Left.Y), rot);
                }
                else
                {
                    Vector3 move = new Vector3();
                    if (keyboard.IsKeyDown(Keys.W) || keyboard.IsKeyDown(Keys.Up))
                        move.Z += 1.0f;
                    if (keyboard.IsKeyDown(Keys.S) || keyboard.IsKeyDown(Keys.Down))
                        move.Z -= 1.0f;
                    if (keyboard.IsKeyDown(Keys.A) || keyboard.IsKeyDown(Keys.Left))
                        move.X += 1.0f;
                    if (keyboard.IsKeyDown(Keys.D) || keyboard.IsKeyDown(Keys.Right))
                        move.X -= 1.0f;
                    viewPosition += Vector3.Transform(new Vector3(movescale, 0, movescale) * move, rot);
                }
            }

            // mouse visibility...
            if (!IsActive || GamePad.GetState(PlayerIndex.One).IsConnected)
                IsMouseVisible = true;
            else
                IsMouseVisible = false;

            // Convert the camera rotation and movement into a view transform.
            return GetViewMatrix();
        }

        /// <summary>
        /// Convert the camera rotation and movement into a view transform.
        /// </summary>
        /// <returns></returns>
        private Matrix GetViewMatrix()
        {
            Matrix rotation = Matrix.CreateFromYawPitchRoll(viewRotation.X, viewRotation.Y, viewRotation.Z);
            Vector3 target = viewPosition + Vector3.Transform(Vector3.Backward, rotation);
            return Matrix.CreateLookAt(viewPosition, target, Vector3.Up);
        }
    }
}
